/**
 * Copyright (C) 2008 University of Pittsburgh
 * 
 * 
 * This file is part of Open EpiCenter
 * 
 *     Open EpiCenter is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 * 
 *     Open EpiCenter is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 * 
 *     You should have received a copy of the GNU General Public License
 *     along with Open EpiCenter.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * 
 *   
 */
package com.hmsinc.epicenter.webapp.chart;

import java.awt.Color;
import java.awt.Font;
import java.awt.GradientPaint;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.annotation.Resource;

import net.sf.ehcache.Cache;
import net.sf.ehcache.Element;

import org.apache.commons.lang.Validate;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.LegendItem;
import org.jfree.chart.LegendItemCollection;
import org.jfree.chart.LegendItemSource;
import org.jfree.chart.annotations.XYAnnotation;
import org.jfree.chart.annotations.XYPointerAnnotation;
import org.jfree.chart.axis.DateAxis;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.block.ColumnArrangement;
import org.jfree.chart.block.LineBorder;
import org.jfree.chart.plot.Marker;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.SeriesRenderingOrder;
import org.jfree.chart.renderer.category.BarRenderer;
import org.jfree.chart.renderer.category.CategoryItemRenderer;
import org.jfree.chart.renderer.xy.XYAreaRenderer;
import org.jfree.chart.renderer.xy.XYBarRenderer;
import org.jfree.chart.renderer.xy.XYDifferenceRenderer;
import org.jfree.chart.renderer.xy.XYItemRenderer;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.chart.title.LegendTitle;
import org.jfree.chart.title.TextTitle;
import org.jfree.data.Range;
import org.jfree.data.category.CategoryDataset;
import org.jfree.data.xy.XYDataset;
import org.jfree.ui.HorizontalAlignment;
import org.jfree.ui.RectangleEdge;
import org.jfree.ui.VerticalAlignment;
import org.springframework.stereotype.Service;

/**
 * Handles rendering, streaming, and caching of charts.
 * 
 * Normally, a chart is generated by passing a "TimeSeriesChart" object to the
 * getChartURL() method. This creates the chart and returns a URL that can be
 * used to access it. After the chart is accessed, it is removed from storage.
 * 
 * @author <a href="mailto:steve.kondik@hmsinc.com">Steve Kondik</a>
 * @version $Id: ChartService.java 1538 2008-04-10 21:17:55Z steve.kondik $
 */
@Service
public class ChartService {

	@Resource
	private Cache chartCache;

	private static final Font LEGEND_FONT = new Font("Arial", Font.PLAIN, 9);

	private static final Font LABEL_FONT = new Font("Arial", Font.PLAIN, 10);

	private static final Font ANNOTATION_FONT = new Font("Arial", Font.BOLD, 10);
	
	private static final String NO_DATA_MESSAGE = "No data to display.";

	/**
	 * @param adapter
	 * @return
	 */
	public String getChartURL(final AbstractChart adapter) {
		return getChartURL(adapter, null);
	}

	/**
	 * @param adapter
	 * @return
	 */
	public String getChartURL(final AbstractChart adapter, final List<XYAnnotation> annotations) {

		Validate.notNull(adapter, "No chart specified.");

		final String uuid = UUID.randomUUID().toString();

		final JFreeChart chart = createChart(adapter, annotations);
		chart.setAntiAlias(true);
		chart.setTextAntiAlias(true);
		
		configureRenderer(chart, adapter);
		configureTitleAndLegend(chart, adapter);		
		
		chartCache.put(new Element(uuid, chart));
		return "chart?id=" + uuid;

	}
	
	/**
	 * @param adapter
	 * @param annotations
	 * @return
	 */
	private static JFreeChart createChart(final AbstractChart adapter, final List<XYAnnotation> annotations) {

		final JFreeChart chart;
		if (adapter.getItems() instanceof XYDataset) {

			chart = ChartFactory.createTimeSeriesChart(adapter.getTitle(), adapter.getXLabel(), adapter.getYLabel(),
					(XYDataset) adapter.getItems(), true, false, false);
			final ValueAxis domainAxis = new DateAxis();
			domainAxis.setLabelFont(LABEL_FONT);
			domainAxis.setTickLabelFont(LABEL_FONT);
			domainAxis.setLowerMargin(0.0);
			domainAxis.setUpperMargin(0.0);

			chart.getXYPlot().setDomainAxis(domainAxis);
			chart.getXYPlot().getRangeAxis().setLabelFont(LABEL_FONT);
			chart.getXYPlot().getRangeAxis().setTickLabelFont(LABEL_FONT);
			chart.getXYPlot().getRangeAxis().setStandardTickUnits(adapter.getRangeTickUnits());
			
			if (adapter.isAlwaysScaleFromZero()) {
				final double upperBound = chart.getXYPlot().getRangeAxis().getRange().getUpperBound();
				final Range range = new Range(0, upperBound + (upperBound * 0.2));
				chart.getXYPlot().getRangeAxis().setRange(range);
			}
			
			for (Marker marker : adapter.getMarkers()) {
				chart.getXYPlot().addDomainMarker(marker);
			}
			
			if (annotations != null) {
				for (XYAnnotation annotation : annotations) {
					if (annotation instanceof XYPointerAnnotation) {
						((XYPointerAnnotation) annotation).setFont(ANNOTATION_FONT);
					}
					chart.getXYPlot().addAnnotation(annotation);
				}
			}

			// Add any bands to the chart
			if (adapter instanceof TimeSeriesChart) {
				final TimeSeriesChart tsc = (TimeSeriesChart)adapter;
				for (int i = 0; i < tsc.getBands().size(); i++) {
					
					final Color c = tsc.getBandColors().get(i);
					final Color fill = tsc.getBandFillColors().get(i);
					final XYDifferenceRenderer renderer = new XYDifferenceRenderer(fill, fill, false);
					renderer.setSeriesPaint(0, c);
					renderer.setSeriesPaint(1, c);
					renderer.setSeriesStroke(0, LineStyle.THICK.getStroke());
					renderer.setSeriesStroke(1, LineStyle.THICK.getStroke());
					
					chart.getXYPlot().setDataset(i + 1, tsc.getBands().get(i));
					chart.getXYPlot().setRenderer(i + 1, renderer);
				}
			}
			
		} else if (adapter.getItems() instanceof CategoryDataset) {

			chart = ChartFactory.createBarChart(adapter.getTitle(), adapter.getXLabel(), adapter.getYLabel(),
					(CategoryDataset) adapter.getItems(), PlotOrientation.VERTICAL, true, false, false);
			chart.getCategoryPlot().getDomainAxis().setLabelFont(LABEL_FONT);
			chart.getCategoryPlot().getRangeAxis().setLabelFont(LABEL_FONT);
			chart.getCategoryPlot().getDomainAxis().setTickLabelFont(LABEL_FONT);
			chart.getCategoryPlot().getRangeAxis().setTickLabelFont(LABEL_FONT);
			
			if (adapter.isAlwaysScaleFromZero()) {
				final double upperBound = chart.getCategoryPlot().getRangeAxis().getRange().getUpperBound();
				final Range range = new Range(0, upperBound + (upperBound * 0.1));
				chart.getCategoryPlot().getRangeAxis().setRange(range);
			}
			
		} else {
			throw new UnsupportedOperationException("Unsupported chart type: " + adapter.getItems().getClass());
		}

		chart.getPlot().setNoDataMessage(NO_DATA_MESSAGE);
		
		return chart;
	}

	/**
	 * @param chart
	 * @return
	 */
	private static void configureTitleAndLegend(final JFreeChart chart, final AbstractChart adapter) {

		final TextTitle title = chart.getTitle();
		if (title != null) {
			title.setFont(new Font("Arial", Font.BOLD, 12));
			// title.setBackgroundPaint(Color.CYAN);
			title.setTextAlignment(HorizontalAlignment.LEFT);
			title.setHorizontalAlignment(HorizontalAlignment.CENTER);
			title.setMargin(0, 4, 5, 6);
		}

		if (chart.getLegend() != null) {
			chart.removeLegend();

			final LegendTitle legend = new LegendTitle(chart.getPlot(), new SNColumnArrangement(0, 0),
					new ColumnArrangement(HorizontalAlignment.CENTER, VerticalAlignment.CENTER, 0, 0));

			legend.setItemFont(LEGEND_FONT);
			legend.setPosition(RectangleEdge.BOTTOM);
			legend.setHorizontalAlignment(HorizontalAlignment.CENTER);
			legend.setBackgroundPaint(Color.WHITE);
			legend.setFrame(new LineBorder());
			legend.setMargin(0, 4, 5, 6);

			chart.addLegend(legend);
			
			
			// Now we'll try to remove any duplicate items from the legend..
			final Map<String, Integer> keys = new HashMap<String, Integer>();
			final LegendItemCollection items = new LegendItemCollection();

			for (LegendItemSource source : legend.getSources()) {

				for (int i = 0; i < source.getLegendItems().getItemCount(); i++) {

					final LegendItem item = source.getLegendItems().get(i);
					if (!keys.containsKey(item.getLabel())) {
						keys.put(item.getLabel(), i);
						items.add(item);
					} 
				}
			}

			legend.setSources(new LegendItemSource[] { new LegendItemSource() {

				/*
				 * (non-Javadoc)
				 * 
				 * @see org.jfree.chart.LegendItemSource#getLegendItems()
				 */
				public LegendItemCollection getLegendItems() {
					return items;
				}

			} });
		}
	}

	/**
	 * @param chart
	 */
	private static void configureRenderer(final JFreeChart chart, final AbstractChart adapter) {

		if (ChartType.BAR.equals(adapter.getType())) {

			chart.getCategoryPlot().setRenderer(getBarRenderer(adapter));

		} else if (ChartType.TIMESERIES_BAR.equals(adapter.getType())) {

			chart.getXYPlot().setRenderer(getTimeSeriesBarRenderer(adapter));

		} else if (ChartType.MOUNTAIN.equals(adapter.getType())) {

			chart.getXYPlot().setRenderer(getMountainRenderer(adapter));
			chart.getXYPlot().setSeriesRenderingOrder(SeriesRenderingOrder.FORWARD);
			
		} else {
			
			final XYItemRenderer renderer = chart.getXYPlot().getRenderer();
		
			for (int i = 0; i < adapter.getColors().size(); i++) {

				final Color c = adapter.getColors().get(i);
				renderer.setSeriesPaint(i, c);
				renderer.setSeriesOutlinePaint(i, c.brighter());
			}
			
			for (int i = 0; i < adapter.getStrokes().size(); i++) {
				renderer.setSeriesStroke(i, adapter.getStrokes().get(i));
			}
			
			if (renderer instanceof XYLineAndShapeRenderer) {
				((XYLineAndShapeRenderer)renderer).setDrawSeriesLineAsPath(true);
			}

		}

	}

	/**
	 * @param chart
	 * @return
	 */
	private static XYAreaRenderer getMountainRenderer(final AbstractChart chart) {

		final XYAreaRenderer renderer = new XYAreaRenderer();
		renderer.setOutline(true);
		
		for (int i = 0; i < chart.getColors().size(); i++) {

			final Color c = chart.getColors().get(i);
			renderer.setSeriesPaint(i, c);
			renderer.setSeriesOutlinePaint(i, c.darker());
			renderer.setSeriesOutlineStroke(i, LineStyle.THICK.getStroke());
			
		}
		
		return renderer;

	}
	
	/**
	 * @param chart
	 * @return
	 */
	private static CategoryItemRenderer getBarRenderer(final AbstractChart chart) {

		final BarRenderer renderer = new BarRenderer();
		renderer.setMaximumBarWidth(0.3);

		for (int i = 0; i < chart.getColors().size(); i++) {

			final Color c = chart.getColors().get(i);
			renderer.setSeriesPaint(i, new GradientPaint(0f, 0f, c, 0f, 0f, c.brighter().brighter()));
			renderer.setSeriesOutlinePaint(i, c);

		}
				
		return renderer;
	}

	/**
	 * @param chart
	 * @return
	 */
	private static XYBarRenderer getTimeSeriesBarRenderer(final AbstractChart chart) {

		final XYBarRenderer renderer = new XYBarRenderer();
		renderer.setMargin(0.2);

		for (int i = 0; i < chart.getColors().size(); i++) {

			final Color c = chart.getColors().get(i);
			renderer.setSeriesPaint(i, new GradientPaint(0f, 0f, c, 0f, 0f, c.brighter().brighter()));
			renderer.setSeriesOutlinePaint(i, c);

		}
		return renderer;
	}

}
